# https://zalinux.ru/?p=554

# Уроки по Awk

## Часть первая: Так что такое AWK?
Awk, главным образом, это потоковый редактор вроде sed. Вы можете передавать по трубе текст в эту программу, и она может манипулировать им построчно. Программа также может читать из файла. Ещё awk – это язык программирования. Это в основном означает, что awk может делать всё, что может sed, а также ещё многое другое.

В отличие от sed, awk может помнить контекст, делать сравнения и многие другие вещи, которые могут делать другие языки программирования. Например, она не ограничена единичной строкой. При надлежащей сноровке, она может ОБЪЕДИНЯТЬ множество строк.

Самая простая форма awk выглядит так:

    awk '{ здесь_какое-то_действие }'
"Здесь_какое-то_действие" может быть просто выражением для печати результата или чем-то намного более сложным. Синтаксис похож на язык программирования 'C'. Простой пример:

    awk '{print $1,$3}'
означает напечатать первый и третий столбец, где под столбцами понимаются «вещи, разделённые белым пробелом». Белый пробел = табуляция или пробел.

Живой пример:

    echo '1 2 3 4' | awk '{print $1,$3}'
    1 3

## Часть вторая: Что может делать AWK?
Главная цель в жизни AWK – это манипулировать её вводом на построчной основе. Программа awk обычно работает в стиле

    Обработать строку. Двигаться дальше
    Обработать строку. Двигаться дальше
    Обработать строку…
Если то, что вы хотите сделать, не вписывается в эту модель, значит awk, может быть, не подходит к вашей задумке.

Обычный используемый в программировании awk синтаксис можно описать так:

    awk образец {команда(ы)}
Это означает, что

«Посмотреть на каждую строку ввода, нет ли там ОБРАЗЦА. Если он там есть, запустить то, что между {}»

Можно пропустить или ОБРАЗЕЦ или КОМАНДУ

Если не указать образец, то команда будет применяться к КАЖДОЙ строке.

Если пропущена команда, то это эквивалентно указанию (просто напечатать строку):

    { print }
Конкретные примеры:

    awk '/#/ {print "В этой строке есть комментарий"}' /etc/hosts
будет печатать «В этой строке есть комментарий» для каждой строки, которая содержит хотя бы один '#' в любом месте строки в /etc/hosts

Модификация для наглядности

1
awk '/#/ {print $0 ":\tВ этой строке есть комментарий"}' /etc/hosts
Элемент '//' в образце – это один из способов задать совпадение. Есть также другие способы задать, совпадает ли строка. Например,

1
awk '$1 == "#" {print "строка начинается с хеша"}' /etc/hosts
будет соответствовать строкам, первый столбец в которых является единичным '#'. Последовательность символов '==' означает ТОЧНОЕ СОВПАДЕНИЕ ВСЕГО первого столбца.

Модификация для наглядности:

1
awk '$1 == "#" {print $0 "\tстрока начинается с хеша"}' /etc/hosts
С другой стороны, если вы хотите частичное совпадение конкретного столбца, используйте оператор '~'

1
awk '$1 ~ /#/ {print "ГДЕ-ТО в столбце 1 есть хеш"}' /etc/hosts
ПОМНИТЕ, ЧТО ПЕРВЫЙ СТОЛБЕЦ МОЖЕТ БЫТЬ ПОСЛЕ БЕЛОГО ПРОБЕЛА.

Модификация для наглядности:

1
awk '$1 ~ /#/ {print $0 "\tГДЕ-ТО в столбце 1 есть хеш"}' /etc/hosts
Ввод "# comment" будет соответствовать

Ввод " # comment" будет ТАКЖЕ соответствовать

Если вам нужно конкретное совпадение «строка, которая начинается точно с # и пробела», вы должны использовать

1
awk '/^# / {делай что-то}'
Множественное совпадение

Awk обработает ВСЕ ОБРАЗЦЫ, которые соответствуют текущей строке. Поэтому если использовать следующий пример,

1
2
3
4
5
awk '
   /#/ {print "Есть комментарий"}
   $1 == "#" {print "Комментарий в первом столбце"}
   /^# /  {print "Комментарий в самом начале"}
 ' /etc/hosts
ТРИ записи будет выведено для строки вроде следующей:

1
# This is a comment
ДВЕ записи для

1
  # This is an indented comment
и только одна для

1
1.2.3.4 hostname # a final comment
Отслеживание контекста

Не все строки созданы равными, даже если они выглядят одинаково. Иногда вы хотите сделать что-то со строкой в зависимости от строк, которые идут перед ней.

Здесь быстрый пример, который печатает строки "ADDR" если вы не в секции "secret"

1
2
3
4
5
awk '
 
/secretstart/    { secret=1}
/ADDR/       { if(secret==0) print $0 } /* $0 – это полная строка */
/secretend/      { secret=0} '
Следующее напечатает содержимое, которое содержит внутри "ADDR" кроме случаев, если была увидена строка "secretstart". ПОРЯДОК ИМЕЕТ ЗНАЧЕНИЕ. Например, если записать так:

1
2
3
4
5
6
awk '
 
/ADDR/       { if(secret==0) print $0 } /* $0 – это полная строка */
/secretstart/    { secret=1}
 
/secretend/      { secret=0} '
и дать следующий ввод

1
2
3
4
5
6
ADDR a normal addr
secretstart ADDR a secret addr
ADDR another secret addr
a third secret ADDR
secretend
ADDR normal too
то будет напечатан первый "secret" addr. При том, что первоначальный пример скроет оба секрета.

Часть третья: Специальные переменные
Мы уже сказали про обычный синтаксис awk. Сейчас давайте начнём рассматривать модные штуки.

awk имеет «специальные» строки соответствия: "BEGIN" и "END"

Директива BEGIN вызывается однажды перед чтением каких-либо строк из данных, никогда снова.

Директива END вызывается после прочтения всех строк. Если дано несколько файлов, то она вызывается только после завершения самого последнего файла.

Обычно вы будете использовать BEGIN для различной инициализации, а END для подведения итогов или очистки.

Пример:

1
2
3
4
BEGIN       { maxerrors=3 ; logfile=/var/log/something ; tmpfile=/tmp/blah}
  ...       { blah blah blah }
/^header/   { headercount += 1 }
END     { printf("всего подсчитано заголовков=%d\n", headercount);
Этот пример посчитает количество раз, которое встречается "header" в файле ввода и напечатает общее количество только после завершения обработки всего файла.

AWK также имеет множество других специальных величин, которые вы можете использовать в секции { }. Например,

1
print  NF
даст вам общее количество колонок (Number of Fields – Количество полей) в текущей строке. FILENAME будет текущим именем файла, подразумевается, что имя файла было передано в awk, а не использована труба.

Вы НЕ МОЖЕТЕ ИЗМЕНИТЬ NF самостоятельно.

Аналогично с переменной NR, которая говорит, как много строк вы обработали. ("Number of Records" – Количество записей)

Есть и другие специальные переменные, вы даже такие, которые вы МОЖЕТЕ изменить в середине программы.

Часть четвёртая: Простые примеры Awk
Чтобы проиллюстрировать и закрепить сказанное, давайте рассмотрим несколько конкретных примеров. Для них нам понадобятся три небольших текстовых файла.

Для последующих примеров давайте создадим файл field_data.txt со следующим содержимым:

1
2
3
4
Roses are red,
Violets are blue,
Sugar is sweet,
And so are you.
В командной строке это можно сделать так:

1
echo -e "Roses are red,\nViolets are blue,\nSugar is sweet,\nAnd so are you." > field_data.txt
Создадим файл letters.txt следующего содержания

1
2
3
4
5
6
7
a
bb
ccc
dddd
ggg
hh
i
В командной строке это можно сделать так:

1
echo -e "a\nbb\nccc\ndddd\nggg\nhh\ni" > letters.txt
И, наконец, создадим файл mail-data со следующим содержимым:

1
2
3
4
5
6
7
8
9
10
11
Amelia       555-5553     amelia.zodiacusque@gmail.com    F
Anthony      555-3412     anthony.asserturo@hotmail.com   A
Becky        555-7685     becky.algebrarum@gmail.com      A
Bill         555-1675     bill.drowning@hotmail.com       A
Broderick    555-0542     broderick.aliquotiens@yahoo.com R
Camilla      555-2912     camilla.infusarum@skynet.be     R
Fabius       555-1234     fabius.undevicesimus@ucb.edu    F
Julie        555-6699     julie.perscrutabor@skeeve.com   F
Martin       555-6480     martin.codicibus@hotmail.com    A
Samuel       555-3430     samuel.lanceolis@shu.edu        A
Jean-Paul    555-2127     jeanpaul.campanorum@nyu.edu     R
Это можно сделать в командной строке так:

1
wget https://raw.githubusercontent.com/tdhopper/awk-lessons/master/data/mail-data -O mail-data
Простой паттерн (образец)

Если нам нужны строки длиннее, чем два символа, и мы хотим использовать действие по умолчанию (print), то мы получаем:

1
2
3
4
5
6
awk 'length $0 > 2' letters.txt
bb
ccc
dddd
ggg
hh
$0 – это встроенная переменная, которая содержит строку.

Простая функция

Если мы не указываем образец, то соответствовать будет каждая строка. Тривиальным действием будет напечатать каждую строку:

1
2
3
4
5
6
7
8
awk '{ print }' letters.txt
a
bb
ccc
dddd
ggg
hh
i
Используя функцию length в качестве нашего действия, мы можем получить длину каждой строки:

1
2
3
4
5
6
7
8
awk '{ print length }' letters.txt
1
2
3
4
3
2
1
Это действие применяется безоговорочно к целой строке. Мы также можем указать это явно:

1
2
3
4
5
6
7
8
awk '{ print length $0 }' letters.txt
1a
2bb
3ccc
4dddd
3ggg
2hh
1i
Awk имеет специальные элементы управления для выполнения некоторого кода перед началом ввода файла и после его завершения.

1
2
3
4
5
6
7
8
9
10
awk 'BEGIN { print "HI" } { print $0 } END { print "BYE!" }' letters.txt
HI
a
bb
ccc
dddd
ggg
hh
i
BYE!
Мы можем иметь больше элементов управления во время печати используя printf.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
awk 'BEGIN { printf "%-10s %s\n", "Name", "Number" \
              printf "%-10s %s\n", "----", "------" } \
            { printf "%-10s %s\n", $1, $2 }' mail-data
Name       Number
----       ------
Amelia     555-5553
Anthony    555-3412
Becky      555-7685
Bill       555-1675
Broderick  555-0542
Camilla    555-2912
Fabius     555-1234
Julie      555-6699
Martin     555-6480
Samuel     555-3430
Jean-Paul  555-2127
Совмещаем образцы и функции

Конечно, паттерны и функции можно совмещать так, что функция будет применяться только если строка соответствует образцу.

Мы можем напечатать длину всех строк, длиннее 2 символов.

1
2
3
4
awk 'length($0) > 2 { print length($0) }' letters.txt
3
4
3
На самом деле, мы не обязаны ограничивать Awk только одним паттерном! Мы можем иметь произвольное количество образцов, разграниченных точкой с запятой или новой строкой:

1
2
3
4
5
6
awk 'length($0) > 2 { print "Long:  " length($0) }; length($0) < 2 { print "Short: " length($0) }' letters.txt
Short: 1
Long:  3
Long:  4
Long:  3
Short: 1
Множество полей

Awk предназначена для простой обработки данных с множеством полей в ряду. Разделитель полей может быть указан ключом -F.

Пример файла, где разделителем является пробел:

1
2
3
4
5
awk '{ print }' field_data.txt
Roses are red,
Violets are blue,
Sugar is sweet,
And so are you.
Если мы указываем разделитель полей, мы можем напечатать второе поле каждой строки:

1
2
3
4
5
awk -F " " '{ print $2 }' field_data.txt
are
are
is
so
Мы не получим ошибку, если строка не имеет соответствующего поля; нам просто будет показана пустая строка:

1
2
3
4
5
awk -F " " '{ print $4 }' field_data.txt
 
 
 
you.
Поскольку по умолчанию разделителем и так является пробел, то предыдущая команда дала бы точно такой же результат и без использования опции -F. Для более осмысленного примера, создадим ещё один файл rates.txt со следующим содержимым:

1
2
3
4
5
Pilcrow,Humphrey,3
Pilcrow,Zora,1
Plinius,Oldone,4
Razniecki,Anton,7
Russell,Bertrand,0
Теперь в качестве разделителя укажем , (запятую) и выведем содержимое второго столбца:

1
2
3
4
5
6
awk -F "," '{ print $2 }' rates.txt
Humphrey
Zora
Oldone
Anton
Bertrand
Выражение разделителя интерпретируется как регулярное выражение.

1
2
3
4
5
6
7
8
9
awk -F "((so )?are|is) " '{print "Field 1: " $1 "\nField 2: " $2}' field_data.txt
Field 1: Roses 
Field 2: red,
Field 1: Violets 
Field 2: blue,
Field 1: Sugar 
Field 2: sweet,
Field 1: And 
Field 2: you.
Регулярные выражения

Образцы (паттерны) могут быть регулярными выражениями, а не только встроенными функциями.

Мы можем использовать регулярные выражения для поиска всех слов в мире Unix с 5 гласными подряд.

1
2
3
4
5
awk '/[aeiou]{5}/' /usr/share/dict/words
cadiueio
Chaouia
euouae
Guauaenok
Передача переменных в программу

Опция -v для Awk позволяет нам передать переменные в программу. Например, мы можем использовать это для жёстких констант кода.

1
2
awk -v pi=3.1415 'BEGIN { print pi }'
3.1415
Мы также можем использовать -v для передачи переменных Bash как переменных Awk

1
2
awk -v user=$USER 'BEGIN { print user }'
mial
Выражения If-else

If-else выражения в Awk имеют вид:

1
if (условие) тело-тогда [else тело-ещё]
Например:

1
2
3
4
5
6
7
8
9
printf "1\n2\n3\n4" | awk \
     '{ \
         if ($1 % 2 == 0) print $1, "is even"; \
         else print $1, "is odd" \
      }'
1 is odd
2 is even
3 is odd
4 is even
Циклы

Awk включает несколько выражений цикла: while, do while и for.

Они имеют ожидаемый синтаксис C.

1
2
3
4
5
6
7
8
9
10
awk \
     'BEGIN { \
         i = 0; \
         while (i < 5) { print i; i+=1; } \
      }'
0
1
2
3
4
1
2
3
4
5
6
awk \
     'BEGIN { \
         i = 0; \
         do { print i; i+=1; } while(i < 0) \
      }'
0
1
2
3
4
5
6
7
8
9
10
awk \
     'BEGIN { \
         i = 0; \
         for(i = 0; i<5; i++) print i \
      }'
0
1
2
3
4
for также может задавать цикл через ключи массива, which будет рассмотрена позже.

Часть пятая: Вызов функций
Следующий компонент AWK – это все его специальные встроенные функции.

AWK имеет функции, которые сделают среднего C программиста весьма счастливым. Здесь такое добро как sin()/cos()/tan(), rand(),index(), sprintf(), tolower(), system()

Функции сгруппированы, их можно рассматривать следующим образом:

Математические

1
+, -, /, *, sin(), cos(), tan(), atan(), sqrt(), rand(), srand()
Они сами за себя говорят, по крайней мере, мне хочется так думать.

Пример:

1
2
awk -v pi=3.1415 'BEGIN { print exp(1), log(exp(1)), sqrt(2), sin(pi), cos(pi), atan2(pi, 2) }'
2.71828 1 1.41421 9.26536e-05 -1 1.00387
Программа может сгенерировать случайное число в диапазоне (0, 1).

1
2
3
awk 'BEGIN { print rand(); print rand() }'
0.237788
0.291066
По умолчанию Awk начинает с одного и того же начала (сида) для Awk. Запуск этой команды два раза подряд возвратит одинаковый результат:

1
2
3
awk 'BEGIN { print rand(); print rand() }'
0.237788
0.291066
Для установки начала (сида) можно использовать функцию srand:

1
2
3
4
5
6
awk 'BEGIN { srand(10); print rand(); print rand() }'
0.255219
0.898883
awk 'BEGIN { srand(10); print rand(); print rand() }'
0.255219
0.898883
Функция int возвращает "самое близкое целое число к x, расположенное между x и нулём и с отброшенным ведущим нулём".

1
2
3
awk 'BEGIN { print "int(0.9)  =  " int(0.9); print "int(-0.9) = " int(-0.9) }'
int(0.9)  =  0
int(-0.9) = 0
Манипуляция строками

index() скажет вам, встречается ли, а если да, то где, строка в подстроке.
match() похожая, но работает для регулярных выражений.
sprintf() даёт вам способы форматировать вывод и по пути делать преобразования. Это должно быть знакомо всем, кто использовал printf() с C. Например,
1
2
newstring=sprintf("one is a number %d, two is a string %s\n", one,  two);
print  newstring
"%d" говорит "напечатай значение, соответствующее мне, в виде десятичного числа"
"%s" говорит "напечатай значение, соответствующее мне, в виде строки"

Т.е. если вы хотите объединить две строки без разрывов, ОДИН из способов будет использование 

1
newstring=sprintf("%s%s", one,  two)
length() просто даёт вам простой способ подсчитать количество символов в строке, если вам это понадобится.
substr

Функция substr(s, m, n) возвратит подстроку в n-символов, начинающуюся с позиции m, отсчитанной от 1.

1
2
3
4
5
awk '{ print $1, substr($1, 2, 3) }' field_data.txt
Roses ose
Violets iol
Sugar uga
And nd
index

index(s, t) возвращает `позицию в s в которой встречается строка t, или 0 если она не встречается.`

Паттерн для index не является регулярным выражением.

1
2
3
4
5
awk '{ print $1, index($1, "s") }' field_data.txt
Roses 3
Violets 7
Sugar 0
And 0
match

match(s, r) возвращает позицию в s в которой встречается регулярное выражение r, или 0 если оно не встречается. Переменные RSTART и RLENGTH устанавливаются в позицию и длину совпадающей строки.

match – это как index кроме того, что паттерн является регулярным выражением.

1
2
3
4
5
awk '{ print $1, match($1, "[sS]") }' field_data.txt 
Roses 3
Violets 7
Sugar 1
And 0
1
2
3
4
5
6
7
8
9
# "Поиск трёх или более повторяющихся букв"
awk '{ match($1, "[a-z]{3}"); print $1, "\tpattern start:", RSTART, "\tpattern end:", RLENGTH }' letters.txt
a   pattern start: 0    pattern end: -1
bb  pattern start: 0    pattern end: -1
ccc     pattern start: 1    pattern end: 3
dddd    pattern start: 1    pattern end: 3
ggg     pattern start: 1    pattern end: 3
hh  pattern start: 0    pattern end: -1
i   pattern start: 0    pattern end: -1
split

split(s, a, fs) расщепляет строку на массив элементов a[1], a[2], …, a[n], и возвращает n.

Разделение делается по регулярному выражению fs или с разделителем полей FS, если fs не дан. Пустая строка в качестве разделителя полей расщепляет строку в массив элементов посимвольно.

1
2
awk 'BEGIN { print split("It-was_the-best_of-times", output_array, "[-_]"), output_array[2], output_array[4] }'
6 was best
sub

sub(r, t, s) заменяет на t первое вхождение регулярного выражения r в строке s. Если не дана s, то используется $0

s является строкой, в которой происходит замена. Вместо возврата новой строки с произведённой заменой будет возвращено количество сделанных замен (0 или 1).

1
2
3
4
5
awk 'BEGIN { s = "It was the best of times, it was the worst of times"; \
             print "Num. matches replaced:",  sub("times", "gifs", s ); \
             print s  }'
Num. matches replaced: 1
It was the best of gifs, it was the worst of times
gsub

gsub делает то же самое, что и sub за исключением того, что заменяются все вхождения регулярного выражения; sub и gsub возвращают количество замен.

1
2
3
4
5
6
awk 'BEGIN { s = "It was the best of times, it was the worst of times"; \
             print "Num. matches replaced:", gsub("times", "cats", s ); \
             print s  }'
 
Num. matches replaced: 2
It was the best of cats, it was the worst of cats
1
2
3
4
5
sprintf
sprintf(fmt, expr, ... ) returns the string resulting from formatting expr ...  according to the printf(3) format fmt
 
awk 'BEGIN { x = sprintf("[%8.3f]", 3.141592654); print x }'
[   3.142]
Функции уровня системы

system() позволяет вам вызвать потенциально ЛЮБОЙ исполнимый файл в системе. Целевая программа может как быть в вашей $PATH, или вы можете указать её по абсолютному пути.

Например, страшное

1
system("rm -rf $HOME");
или

1
system("/bin/kill 1")
Если вы хотите делать более сложные вещи, вы, вероятно, в конечном итоге делает что-то вроде

1
2
sysstring=sprintf("somecommand %s %s", arg1,  arg2);
system(sysstring)
close() – это важная функция, которую часто упускают из вида. Вероятно, это из-за того, что нет очевидного вызова open(), поэтому народ не думает про вызов close(). И для большинства целей это и не нужно. Но вы ДОЛЖНЫ ДЕЛАТЬ ЭТО, если вы имеете дело с боее чем одним файлом вывода.

Awk даёт вам возможность открыть произвольный файл на лету. Например

1
/^file/   { print $3 >> $2 }
должен взять строку "file output here-is-a-word", открыть файл 'output' и напечатать 'here-is-a-word' в него.

AWK является "умной", в том, что отслеживает, какие файлы вы открывайте и СОХРАНЯЕТ их открытыми. Она предполагает, если вы открыли файл один раз, вы, вероятно, сделать это снова. К сожалению, это означает, что, если вы откроете МНОГО файлов, файловые дескрипторы могут закончиться. Поэтому, когда вы знаете, что закончили с файлом, закройте его. Поэтому для улучшения примера выше, вы должны использовать что-то в духе следующих строк:

1
2
/^file/   { if ( $2 !=  oldfile ) { close( oldfile) };
          print $3 >> $2 ;  oldfile = $2; }
Часть шестая: Массивы
Понятие массива

Мы уже рассмотрели переменные как имена, которые содержат значение. Массивы являются продолжением переменных. Массивы – это переменные, которые содержат более одного значения. Они могут содержать более чем одно значение поскольку каждое это значение имеет свой номер.

Если вам нужно иметь три значения, вы можете сказать:

1
value1="one"; value2="two"; value3="three";
ИЛИ, вы можете использовать

1
values[1]="one"; values[2]="two"; values[3]="three";
Первый пример – это три разных переменных со своими именами (которые различаются на один символ). Второй пример – это массив, который состоит из одной переменной, но содержит много значение, каждое из которых имеет свой номер.

При использовании переменной в качестве массива, всегда нужно помещать значение в квадратные скобки []. Вы можете выбрать любое имя для переменной массива, но с этого момента это имя можно использовать ТОЛЬКО для массива. Вы НЕ МОЖЕТЕ осмысленно делать

1
2
values[1]="one";
values="newvalue";
Тем не менее, вы МОЖЕТЕ переназначить величины, как для нормальных переменных. Т.е. следующее ЯВЛЯЕТСЯ правильным:

1
2
3
4
values[1]="1";
print values[1];
values[1]="one";
print values[1];
Интересно то, что в отличие от некоторых других языков, вы не обязаны использовать только номера. В примерах выше [1],[2],[3] на самом деле истолковываются как [«1»], [«2»], [«3»]. Что означает, что вы также можете использовать другие строки в качестве идентификаторов, и рассматривать массив почти как базу данных с одной колонкой. Официальное название для этого «ассоциированный массив».

1
2
3
4
5
6
7
numbers["one"]=1;
numbers["two"]=2;
print numbers["one"];
value="two";
print numbers[value];
value=$1;
if(numbers[value] = ""){ print "no such number"; }
Когда и как использовать массивы

Могут быть различные случае, когда вы можете выбрать использование массивов. Некоторые при работе с awk вообще обходятся без массивов. Но это не совсем верная позиция: для массивов существуют специальные переменные, которые, например, показывают его размер (количество значений в массиве), имеются удобные для перебора членов массива конструкции, некоторые функции возвращают значение в виде массива. В любом случае, давайте рассмотрим несколько примеров, которые могут пригодиться.

Сохранение информации для дальнейшего использования

При использовании awk в большом скрипте оболочки, можно сохранять информацию во временный файл. Но можно сохранять нужные слова в память, а затем все их напечатать в конце, что будет быстрее, чем использование временного файла.

1
2
3
4
5
6
7
8
9
/special/{ savedwords[lnum]=$2; lnum+=1; }
END {
        count=0;
        while(savedwords[count] != "")
        {
            print count,savedwords[count];
            count+=1;
        }
    }
Вместо простого вывода слов, вы можете использовать секцию END чтобы перед их отображением сделать дополнительную обработку, которая вам может быть нужна.

Если вы хотите присвоить уникальный индекс значениям (для избегания дубликатов), вы вообще можете отсылать к их значения по их собственным строкам. Или, например, сохранить массив с колонкой 3, проиндексированный по соответствующему значению колонки 2.

1
2
3
4
5
6
7
{ threecol[$2]=$3 }
END {
        for (v in threecol)
        {
            print v, threecol[v]
        }
    }
Массивы и функция split()

Другая главная причина для использования массивов, если вы хотите делать подполя. Допустим у вас есть строка, которая имеет несколько крупных подразделений и несколько мелких подразделений. Другими словами, Поля верхнего уровня разделяются пробелами, но затем вы получаете более маленькие слова, разделённые двоеточиями.

1
2
This is a variable:field:type line
There can be multiple:type:values here
В примере выше четвёртое отделённое пробелом поле имеет подполя, разделённые двоеточиями. Теперь, допустим, вы хотите узнать значение второго подполя в четвёртом большом поле. Один из способов сделать это, вызвать две awk связанные трубой:

1
awk '{print $4}' | awk -F: '{print $2}'
Другим способом будет на лету изменить значение 'FS', которая содержит разделитель полей (судя по всему, это работает не со всеми реализациями awk):

1
awk '{ newline=$4; fs=FS; FS=":";  $0=newline; print $2 ; FS=fs; }'
Но вы также можете сделать это с массивами, используя функцию split() следующим образом:

1
awk '{ newline=$4; split(newline,subfields,":"); print subfields[2]} '
В этом случае использование массива это самый обычный и, возможно, самый изящный способ сделать это.

Итак, Awk обеспечивает ограниченное количество структур данных. Кроме скалярных и строковых переменных, в язык встроена структура массивных данных. Хотя официально она называется «массивы», эта структура на самом деле является ассоциированным массивом, аналогичной структуре данных dict в Python.

Массивы не нужно инициализировать. Вы можете просто начать присваивать значения. Обратите внимание, что ключами могут быть цифры или строки. 

1
2
3
4
5
6
7
awk 'BEGIN { \
      a[0] = 1.1; \
      a[0.83] = 0; \
      a["DOG"] = "CAT"; \
      print a[0], a[0.83], a["DOG"] \
     }'
1.1 0 CAT
Awk не будет печать переменную без индекса:

1
2
3
4
5
awk 'BEGIN { \
      a["DOG"] = "CAT"; \
      print a \
     }'
awk: cmd. line:3: fatal: attempt to use array `a' in a scalar context
Хотя мы можем сделать цикл по ключу используя for:

1
2
3
4
5
6
7
8
9
awk 'BEGIN { \
      a[0] = 1.1; \
      a[0.83] = 0; \
      a["DOG"] = "CAT"; \
      for(k in a) print(a[k]) \
      }'
CAT
0
1.1
Часть седьмая: AWK и оболочки (sh/ksh/bash/csh)
Иногда функционала AWK может быть недостаточно. В этом случае можно интегрировать awk в скрипт оболочки. Далее несколько примеров как это можно сделать.

Простой вывод

Иногда хочется использовать awk просто как программу форматирования, и сбрасывать вывод прямо пользователю Следующий скрипт принимает в качества аргумента имя пользователя и использует awk для дампа информации о нём из /etc/passwd.

Примечание: обратите внимание, что в скрипте одинарные кавычки раскрываются (а не являются вложенными) и между двумя раскрытыми парами одинарных кавычек стоит переменная $1 (вторая), которая в данном случае является аргументом скрипта, в то время как $1 является частью синтаксиса $1 (означает первое поле в строке).

1
2
3
4
5
6
#!/bin/sh
 
while [ "$1" != "" ] ; do
    awk -F: '$1 == "'$1'" { print $1,$3} ' /etc/passwd
    shift
done
Присвоение переменным оболочки вывода awk

Иногда мы хотим использовать awk просто для быстрого способа установить значение переменной. Используя тему passwd, у нас есть способ узнать шелл для пользователя и увидеть, входит ли он в список официальных оболочек.

И опять, обратите внимание, как происходит закрытие одинарных кавычек в выражении awk, После закрытой (второй) кавычки, $1 является переменной, в которую передано значение первого аргумента скрипта, а не частью синтаксиса awk.

1
2
3
4
5
6
7
8
9
10
#!/bin/sh
 
user="$1"
if [ "$user" == "" ] ; then echo ERROR: need a username ; exit ; fi
 
usershell=`awk -F: '$1 == "'$1'" { print $7} ' /etc/passwd`
grep -l $usershell /etc/shells
if [ $? -ne 0 ] ; then
        echo ERROR: shell $usershell for user $user not in /etc/shells
fi
Другие альтернативы:

1
2
3
4
5
6
7
# Смотрите "man regex"
usershell=`awk -F: '/^'$1':/ { print $7} ' /etc/passwd`
echo $usershell;
 
# Только современные awk принимают -v. Вам может понадобиться использовать "nawk" или "gawk"
usershell2=`awk -F: -v user=$1 '$1 == user { print $7} ' /etc/passwd`
echo $usershell2;
Объяснение дополнительных вышеприведённых методов остаётся домашним заданием читателю 🙂

Передача данных в awk по трубе

Иногда хочется поместить awk в качестве фильтра данных, в большую программу или команду в одной строке, вводимую в запрос оболочки. Пример такой команды в скрипте (в качестве аргументов скрипта передаётся список файлов логов веб-сервера, поскольку запись в журнал настраивается и логи могут иметь различную структуру, для работоспособности в конкретных случаях может понадобиться подправить команды):

1
2
3
#!/bin/sh
 
grep -h ' /index.html' $* | awk -F\" '{print $4}' | sort -u
Вопросы и ответы по awk
Как вывести только строку определённого номера в awk
Чтобы вывести строки с определённым номером, используйте if() и переменную NR.

Например, чтобы вывести только вторую строку:

1
free | awk '{ if (NR == 2) print $7 }'
Чтобы вывести вторую и все последующие строки:

1
free | awk '{ if (NR >= 2) print $7 }'
Чтобы вывести все строки с 10 по 20:

1
awk '{ if (NR >= 10 && NR <= 20) print }' /etc/passwd
Как перенаправить вывод в файл в awk
Команду print можно использовать с перенаправлением вывода в файл.

К примеру, следующая команда сохранит строки с 10 по 20 из файла /etc/passwd в файл pswd.txt:

1
awk '{ if (NR >= 10 && NR <= 20) print>"pswd.txt" }' /etc/passwd
Следующая команда ищет в строке слово «mial» и если оно там встречается, то сохраняет всю строку в файл pswd.txt:

1
awk '/mial/{ print>"pswd.txt" }' /etc/passwd
Как использовать переменные в awk
Следующая команда посчитает количество строк содержащих слово «bash» в файле /etc/passwd, выведет каждую из этих строк и затем выведет общее количество найденных строк:

1
awk -v y=0 '/bash/{ y++; print $0 } END { print "Всего найдено строк с bash: " y }' /etc/passwd
При запуске программы инициируется переменная y со значением 0. При каждом совпадении (найдена строка «bash»), значение y увеличивается на единицу и выводится найденная строка. В конце выводится значение y.

Как вывести скобки и другие специальные в awk
Скобки и другие специальные символы необходимо помещать в двойные кавычки.

1
awk -F ',' '{ print "(" $1, $2 ")" }'
В предыдущих примерах имя файла также помещено в двойные кавычки из-за содержащейся в нём точки, которая является специальным символом.

Примеры решения задач с помощью awk
Задача 1.
Есть файл вот с таким содержимым:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ОРГАНИЗАЦИЯ|
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
ОРГАНИЗАЦИЯ
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
ОРГАНИЗАЦИЯ
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
ОРГАНИЗАЦИЯ
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
ОРГАНИЗАЦИЯ
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
ОРГАНИЗАЦИЯ
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
НАЧИСЛЕНИЕ|
Таких строк может быть любое количество, как в меньшую, так и в большую сторону. После слов "ОРГАНИЗАЦИЯ" строка продолжается наименованием и реквизитами организации.

После слов "НАЧИСЛЕНИЕ" строка продолжается суммой начисления и прочими данными.

Разделителем является труба: |

Задача такая — с помощью awk найти слово "ОРГАНИЗАЦИЯ", скопировать содержимое этой строки и привязанные к ней строки "НАЧИСЛЕНИЕ".

Т.е. первую строку "ОРГАНИЗАЦИЯ" и две строки ниже со словом "НАЧИСЛЕНИЕ". Вывести эти 3 строки в отдельный файл. Количество строк может быть любым.

Найти следующие строки по такому же принципу и опять в отдельный файл.

По этому примеру должно быть 6 файлов и каждый со своей организацией и начислениями.

Решение 1. Работает в awk под Linux
1
awk -v y=0 '/ОРГАНИЗАЦИЯ/{if(x) print x>y; x=$0 ;y++} !/ОРГАНИЗАЦИЯ/{x=x"\r\n"$0} END {print x>y}' test.txt


Решение 2. Работает в awk под Windows
1
awk.exe -v y=0 "/ОРГАНИЗАЦИЯ/{y++; print $0>y\".txt\";} !/ОРГАНИЗАЦИЯ/{print $0>y\".txt\"}" test.txt
Входные данные в файле test.txt. Тестировалась в cmd (а не в PowerShell).

Скриншот:



Содержимое первых четырёх файлов:



Задача 2.
Как добавить в существующий файл строку с нужными мне данными?

Т.е. есть файл, например, с 6 строками и я хочу добавить сроку перед 1 строкой или добавить строку во все .txt файлы в определённой папке.

Решение 1. Работает в awk под Linux
Итак, следующий пример считывает все файлы из текущего каталога (*). Если нужно, чтобы считывал только файлы с определённым разрешением, то вместо звёздочки можно записать, например, так *.txt.

Для каждого обрабатываемого файла в самое начало добавляется строка "СТРОКА" — замените на нужную. Новые файлы сохраняются в эту же папку, с такими же именами, но перед именем добавляется префикс "new_" - при желании, его можно удалить или заменить на другой (в двух местах команды!).

Собственно команда:

1
awk -v 'OLD_FILENAME=""' '{if(OLD_FILENAME!=FILENAME) print "СТРОКА" > "new_"FILENAME; OLD_FILENAME=FILENAME} {print > "new_"FILENAME;}' *
Логика работы:

при запуске awk инициализируется переменная OLD_FILENAME — которой в качестве значения присваивается пустая строка
при обработке каждой строки, значение переменной OLD_FILENAME сравнивается со значением встроенной переменной FILENAME в которой (сюрприз!) содержится имя текущего файла. Если эти значения НЕ равны, то в файлы печатается строка "СТРОКА" И переменной OLD_FILENAME присваивается значение, которое содержит FILENAME.
следующая часть {print > "new_"FILENAME;} просто печатает очередную строку в новый файл
при последующих прохождениях (вторая строка, третья строка и т. д.) значения OLD_FILENAME и FILENAME будут одинаковыми. Вплоть до момента, пока обрабатываемый файл не сменится на новый. В этот момент OLD_FILENAME и FILENAME становятся не равны и в новый файл первой печатается строка "СТРОКА" и переменной OLD_FILENAME присваивается значение FILENAME
всё продолжается, пока все строки во всех файлах не кончатся
Решение 2. Работает в awk под Windows
Для Windows (cmd, а не PowerShell):

1
awk.exe -v OLD_FILENAME="" "{if(OLD_FILENAME!=FILENAME) print \"СТРОКА\" > \"new_\"FILENAME; OLD_FILENAME=FILENAME} {print > \"new_\"FILENAME;}" *.txt

